﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Reflection;
using Fabrication.Extensions;
using Fabrication.ValueProviders;

namespace Fabrication
{
    public class ConventionsProvider
    {
        private readonly Dictionary<Type, Type> typeProviders = new Dictionary<Type, Type>
                                    {
                                        {typeof (int), typeof (SequentialValueProvider)},
                                        {typeof (uint), typeof (SequentialUnsignedValueProvider)},
                                        {typeof (long), typeof (SequentialValueProvider)},
                                        {typeof (ulong), typeof (SequentialUnsignedValueProvider)},
                                        {typeof (string), typeof (SequentialNamingProvider)},
                                        {typeof (Guid), typeof (RandomGuidProvider)},
                                        {typeof (DateTime), typeof (DefaultDateProvider)},
                                        {typeof (bool), typeof (DefaultBooleanProvider)},
                                        {typeof (char), typeof (DefaultCharProvider)},
                                        {typeof (float), typeof (DefaultFloatProvider)},
                                        {typeof (double), typeof (DefaultDoubleProvider)},
                                        {typeof (decimal), typeof (DefaultDecimalProvider)}
                                    };

        private readonly Dictionary<TypedPropertyKey, IValueProvider> typedPropertyProviders = new Dictionary<TypedPropertyKey, IValueProvider>
                                                          {
                                                              { new TypedPropertyKey(typeof(string),  "FirstName"), new GivenNameProvider() },
                                                              { new TypedPropertyKey(typeof(string),  "GivenName"), new GivenNameProvider() },
                                                              { new TypedPropertyKey(typeof(string),  "LastName"), new LastNameProvider() },
                                                              { new TypedPropertyKey(typeof(string),  "Surname"), new LastNameProvider() },
                                                              { new TypedPropertyKey(typeof(string),  "FullName"), new FullNameProvider() },
                                                              { new TypedPropertyKey(typeof(string),  "Email"), new EmailProvider() },
                                                              { new TypedPropertyKey(typeof(string),  "EmailAddress"), new EmailProvider() },
                                                              { new TypedPropertyKey(typeof(string),  "UserName"), new UserNameProvider() },
                                                              { new TypedPropertyKey(typeof(string),  "Country"), new CountryProvider() },
                                                              { new TypedPropertyKey(typeof(string),  "Phone"), new PhoneNumberProvider() },
                                                              { new TypedPropertyKey(typeof(string),  "PhoneNumber"), new PhoneNumberProvider() },
                                                              { new TypedPropertyKey(typeof(string),  "Mobile"), new PhoneNumberProvider() },
                                                              { new TypedPropertyKey(typeof(string),  "MobileNumber"), new PhoneNumberProvider() },
                                                              { new TypedPropertyKey(typeof(char),  "Initial"), new UpperCharProvider() },
                                                              { new TypedPropertyKey(typeof(int),  "Zip"), new PostCodeProvider() },
                                                              { new TypedPropertyKey(typeof(int),  "ZipCode"), new PostCodeProvider() },
                                                              { new TypedPropertyKey(typeof(int),  "PostCode"), new PostCodeProvider() },
                                                              { new TypedPropertyKey(typeof(DateTime),  "DueDate"), new FunctionValueProvider<DateTime>(x => DateTime.Now.AddDays(RandomNumberGenerator.GetDouble(1, 7))) },
                                                              { new TypedPropertyKey(typeof(DateTime),  "DateDue"), new FunctionValueProvider<DateTime>(x => DateTime.Now.AddDays(RandomNumberGenerator.GetDouble(1, 7))) },
                                                              { new TypedPropertyKey(typeof(DateTime),  "ReceivedDate"), new FunctionValueProvider<DateTime>(x => DateTime.Now.AddDays(RandomNumberGenerator.GetDouble(1, 7) * -1)) },
                                                              { new TypedPropertyKey(typeof(DateTime),  "DateReceived"), new FunctionValueProvider<DateTime>(x => DateTime.Now.AddDays(RandomNumberGenerator.GetDouble(1, 7) * -1)) }
                                                          };

        private readonly Dictionary<Type, Type> genericPropertyMatchProviders = new Dictionary<Type, Type>
                                                            {
                                                                { typeof(Dictionary<,>), typeof(DictionaryProvider) },
                                                                { typeof(List<>), typeof(GenericListProvider) },
                                                                { typeof(IList<>), typeof(GenericListProvider) },
                                                                { typeof(IEnumerable<>), typeof(GenericListProvider) },
                                                                { typeof(Collection<>), typeof(GenericCollectionProvider) },
                                                                { typeof(ICollection<>), typeof(GenericCollectionProvider) },
                                                                { typeof(Nullable<>), typeof(NullableProvider) }
                                                            };

        private readonly Dictionary<Predicate<Type>, Type> propertyMatchProviders = new Dictionary<Predicate<Type>, Type>
                                                            {
                                                                { t => t.IsArray, typeof(ArrayProvider) },
                                                                { t => t.IsEnum, typeof(EnumProvider) },
                                                                { t => t.IsValueType, typeof(StructProvider) },
                                                                { t => t.DerivesFrom<StringCollection>(), typeof(StringCollectionProvider) },
                                                                { t => t.DerivesFrom<IEnumerable>(), typeof(NonGenericListProvider) },
                                                                { t => t.DerivesFrom<IList>(), typeof(NonGenericListProvider) }
                                                            };

        public bool TryGetConventionFor(PropertyInfo property, out IValueProvider valueProvider)
        {
            // If there's a rule specified for this property type/name convention then use it
            if (typedPropertyProviders.TryGetValue(new TypedPropertyKey(property.PropertyType, property.Name), out valueProvider))
                return true;

            // If a rule has been specified for this property type then create an instance
            Type valueProviderType;
            if (typeProviders.TryGetValue(property.PropertyType, out valueProviderType))
            {
                valueProvider = (IValueProvider)Activator.CreateInstance(valueProviderType);
                valueProvider.Initialize(property);
                return true;
            }

            // If the property type has a known generic parent then set provider
            if (property.PropertyType.IsGenericType)
            {
                var genericType = property.PropertyType.GetGenericTypeDefinition();
                if (genericPropertyMatchProviders.TryGetValue(genericType, out valueProviderType))
                {
                        valueProvider = (IValueProvider) Activator.CreateInstance(valueProviderType);
                        valueProvider.Initialize(property);
                        return true;
                }
            }

            // If the property type matches a predicate then set provider
            foreach (var interfacePropertyProvider in propertyMatchProviders)
            {
                if (interfacePropertyProvider.Key(property.PropertyType))
                {
                    valueProviderType = interfacePropertyProvider.Value;
                    valueProvider = (IValueProvider)Activator.CreateInstance(valueProviderType);
                    valueProvider.Initialize(property);
                    return true;
                }
            }

            return false;
        }

        public void SetConvention<TParam, TProvider>()
            where TProvider : IValueProvider
        {
            typeProviders.AddOrUpdate(typeof(TParam), typeof(TProvider));
        }

        public void SetConvention<TPropertyType>(IValueProvider provider, string propertyName)
        {
            var key = new TypedPropertyKey(typeof(TPropertyType), propertyName);
            typedPropertyProviders.AddOrUpdate(key, provider);
        }

        public void RemoveConvention(string propertyName)
        {
            var conventionsToRemove = typedPropertyProviders.Keys.Where(k => k.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)).ToList();
            foreach (var key in conventionsToRemove)
                typedPropertyProviders.Remove(key);
        }

        public void RemoveConvention<TPropertyType>(string propertyName)
        {
            var key = new TypedPropertyKey(typeof(TPropertyType), propertyName);
            typedPropertyProviders.Remove(key);
        }

        public void RemoveAllNamedConventions()
        {
            typedPropertyProviders.Clear();
        }
    }
}